from abc import ABC, abstractmethod
from device import BlockDevice
from page import Page
from typing import Dict


class IPageCache(ABC):

    @abstractmethod
    def find(self, bdev: BlockDevice, index: int) -> Page | None:
        pass

    @abstractmethod
    def _hash(self, bdev: BlockDevice, index: int):
        pass

    @abstractmethod
    def _alloc(self, bdev: BlockDevice, index: int) -> Page:
        pass

    @abstractmethod
    def get(self, bdev: BlockDevice, index: int) -> Page:
        pass

    @abstractmethod
    def free(self, bdev: BlockDevice, index: int):
        pass

    @abstractmethod
    def free_range(self, bdev: BlockDevice, start: int, end: int, step: int) -> bool:
        pass
    
    @abstractmethod
    def sync(self) -> bool:
        pass

    @abstractmethod
    def sync_bdev(self, bdev: BlockDevice) -> bool:
        pass

    @abstractmethod
    def shrink(self) -> bool:
        pass
    

class PageCache(IPageCache):
    def __init__(self):
        self.__tab : Dict[int, Page] = {}
        #self.__lru = []

    def _hash(self, bdev: BlockDevice, index: int):
        return hash((id(bdev), index))

    def find(self, bdev: BlockDevice, index: int) -> Page | None:
        return self.__tab.get(self._hash(bdev, index))

    def _alloc(self, bdev: BlockDevice, index: int) -> Page:
        return Page(bdev, index)

    #  r=find  ->  alloc if find-failed, else r
    def get(self, bdev: BlockDevice, index: int) -> Page:
        hcode = self._hash(bdev, index)
        p = self.__tab.get(hcode)
        if p is not None:
            return p
        p = self._alloc(bdev, index)
        self.__tab[hcode] = p
        return p

    def free(self, bdev: BlockDevice, index: int):
        hcode = self._hash(bdev, index)
        p = self.__tab.get(hcode)
        if p is not None:
            del self.__tab[hcode]

    def free_range(self, bdev: BlockDevice, start: int, end: int, step: int) -> bool:
        #TODO: iter the bdev:[start,end) with step=page.size()
        pass
    
    def sync(self) -> bool:
        for p in self.__tab.values():
            if not p.sync() :
                return False
        return True

    def sync_bdev(self, bdev: BlockDevice) -> bool:
        #TODO:
        pass

    def shrink(self) -> bool:
        #TODO:
        pass


import unittest 
from device import MockFloppyDiskDriver
class TestBlockCache(unittest.TestCase):
    def test_main(self):
        bcache = PageCache()
        bdev = MockFloppyDiskDriver()
        index = 0
        
        b = bcache.find(bdev, index)
        self.assertIsNone(b)

        b = bcache.get(bdev, index) # find & alloc if not exists
        self.assertIsNotNone(b)

        b = bcache.find(bdev, index)
        self.assertIsNotNone(b)

        bcache.free(bdev, index) # remove the cache blocks

        b = bcache.find(bdev, index)
        self.assertIsNone(b)

        bcache.sync()

